﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Lilium.Control;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using Lilium.Web.Mvc.Html;
using System.Web.Routing;
using System.Linq.Expressions;

namespace Lilium.Web.Mvc
{
    public static class EntityWorkflowExtensions
    {
        #region Monadic

        public static Either<ActionResult, IQueryable<TEntity>> ApplySort<TWorkflow, TEntity>(this TWorkflow workflow, IQueryable<TEntity> model)
            where TWorkflow : EntityWorkflow
        {
            if (workflow == null) throw new ArgumentNullException("workflow");

            return Execute(workflow, () =>
            {
                var sortExpressionBuilder = workflow.GetEntityService(typeof(TEntity), () => new SortExpressionBuilder(workflow.GetEntityMetadata(typeof(TEntity))));
                return sortExpressionBuilder.AppendSortExpression(model);
            });
        }

        public static Either<ActionResult, TWorkflow> Execute<TWorkflow>(this TWorkflow workflow, Action action)
            where TWorkflow : EntityWorkflow
        {
            if (workflow == null) throw new ArgumentNullException("workflow");
            if (action == null) throw new ArgumentNullException("action");

            try
            {
                action();

                if (workflow.ModelState.IsValid)
                    return workflow;
                else
                    return workflow.GetActionResult();
            }
            catch (Exception e) //TODO: добавить возможность управления политикой обработки исключений.
            {
                workflow.ViewData.SetValidationMessage("Необработанная ошибка в процессе выполнения запроса");
                workflow.ModelState.AddModelError("", e.ToString());
                return workflow.GetActionResult();
            }
        }

        public static Either<ActionResult, TResult> Execute<TWorkflow, TResult>(this TWorkflow workflow, Func<TResult> func)
            where TWorkflow : EntityWorkflow
        {
            if (workflow == null) throw new ArgumentNullException("workflow");
            if (func == null) throw new ArgumentNullException("func");

            try
            {
                var result = func();

                if (workflow.ModelState.IsValid)
                    return result;
                else
                    return workflow.GetActionResult();
            }
            catch (Exception e)
            {
                workflow.ViewData.SetValidationMessage("Необработанная ошибка в процессе выполнения запроса");
                workflow.ModelState.AddModelError("", e.ToString());
                return workflow.GetActionResult();
            }
        }

        public static Either<ActionResult, TEntity> CreateEntity<TWorkflow, TEntity>(this TWorkflow workflow, TEntity entity)
            where TEntity : class
            where TWorkflow : EntityWorkflow
        {
            if (workflow == null) throw new ArgumentNullException("workflow");

            return
                from _ in workflow.Execute(() =>
                    {
                        var repository = workflow.GetRepository<TEntity>();
                        repository.Insert(entity);
                    })
                select entity;
        }

        public static Either<ActionResult, TWorkflow> DeleteEntity<TWorkflow, TEntity>(this TWorkflow workflow, TEntity entity)
            where TEntity : class
            where TWorkflow : EntityWorkflow
        {
            if (workflow == null) throw new ArgumentNullException("workflow");

            return Execute(workflow, () =>
            {
                var repository = workflow.GetRepository<TEntity>();
                repository.Delete(entity);
            });
        }

        public static Either<ActionResult, TWorkflow> DeleteEntities<TWorkflow, TEntity>(this TWorkflow workflow, IEnumerable<TEntity> entities)
            where TEntity : class
            where TWorkflow : EntityWorkflow
        {
            if (workflow == null) throw new ArgumentNullException("workflow");

            return Execute(workflow, () =>
            {
                if (entities == null) throw new ArgumentNullException("entities");

                var repository = workflow.GetRepository<TEntity>();
                foreach (var entity in entities.ToArray())
                    repository.Delete(entity);
            });
        }

        public static Either<ActionResult, TState> GetControllerState<TWorkflow, TState>(this TWorkflow workflow, string key, Func<TState> fallback, bool promoteValuesToViewData)
            where TWorkflow : EntityWorkflow
        {
            if (workflow == null) throw new ArgumentNullException("workflow");

            return workflow.Execute(() =>
                {
                    var stateValue = workflow.Controller.GetControllerState(key, fallback);

                    if (promoteValuesToViewData && (stateValue != null))
                        workflow.ViewData[key] = stateValue;

                    return (TState)stateValue;
                });
        }
        /*
        public static Either<ActionResult, TEntity> LoadEntity<TWorkflow, TEntity>(this TWorkflow workflow, object entityKey, Func<TEntity> fallback)
            where TEntity : class
            where TWorkflow : EntityWorkflow
        {
            return LoadEntity(workflow, entityKey, fallback, ModelRegistrationOptions.None);
        }

        public static Either<ActionResult, TEntity> LoadEntity<TWorkflow, TEntity>(this TWorkflow workflow, object entityKey, TEntity fallbackValue)
            where TEntity : class
            where TWorkflow : EntityWorkflow
        {
            return LoadEntity(workflow, entityKey, () => fallbackValue, ModelRegistrationOptions.None);
        }
        
        public static Either<ActionResult, TEntity> LoadEntity<TWorkflow, TEntity>(
            this TWorkflow workflow, 
            object entityKey, 
            Func<TEntity> fallback, 
            ModelRegistrationOptions registrationOptions)
            where TEntity : class
            where TWorkflow : EntityWorkflow
        {
            if (workflow == null) throw new ArgumentNullException("workflow");

            return Execute(workflow, () =>
            {
                var repository = workflow.GetRepository<TEntity>();
                var entity = repository.Get(entityKey);
                if ((entity == null) && (fallback != null))
                    entity = fallback();
                workflow.RegisterModel(entity, registrationOptions);
                return entity;
            });
        }
        
        public static Either<ActionResult, TEntity> LoadEntity<TWorkflow, TEntity>(this TWorkflow workflow, object entityKey, TEntity fallbackValue, ModelRegistrationOptions registrationOptions)
            where TEntity : class
            where TWorkflow : EntityWorkflow
        {
            return LoadEntity(workflow, entityKey, () => fallbackValue, registrationOptions);
        }
        */
        // Сокращенный вариант без ModelRegistrationOptions. Используется т.к. часто в wf данные загружаются для внутренних нудж.
        public static Either<ActionResult, TEntity> LoadEntity<TWorkflow, TEntity>(
            this TWorkflow workflow,
            Expression<Func<TEntity, bool>> predicate,
            Func<Tuple<string, string>> errorProvider)
            where TEntity : class
            where TWorkflow : EntityWorkflow
        {
            return LoadEntity(workflow, predicate, errorProvider, ModelRegistrationOptions.None);
        }

        //Вот это кажется самый лучший вариант. От остальных следует избавляться.
        public static Either<ActionResult, TEntity> LoadEntity<TWorkflow, TEntity>(
            this TWorkflow workflow, 
            Expression<Func<TEntity, bool>> predicate,
            Func<Tuple<string, string>> errorProvider,
            ModelRegistrationOptions registrationOptions)
            where TEntity : class
            where TWorkflow : EntityWorkflow
        {
            if (workflow == null) throw new ArgumentNullException("workflow");
            if (errorProvider == null) throw new ArgumentNullException("errorProvider");

            return LoadModel(
                workflow,
                (IQueryable<TEntity> entity) => entity.FirstOrDefault(predicate),
                () =>
                {
                    var error = errorProvider();
                    workflow.AddModelError(error.Item1, error.Item2);
                    return default(TEntity);
                },
                registrationOptions);
        }
        
        // [Obsolete("Не надо использовать функцию без явного указания ModelRegistrationOptions, это ведет к путанице")] уже не ведет, не указана - значит none.
        public static Either<ActionResult, TModel> LoadModel<TWorkflow, TEntity, TModel>(this TWorkflow workflow, Func<IQueryable<TEntity>, TModel> selector)
            where TEntity : class
            where TWorkflow : EntityWorkflow
        {
            return LoadModel(workflow, selector, null, ModelRegistrationOptions.None);
        }
        
        public static Either<ActionResult, TModel> LoadModel<TWorkflow, TEntity, TModel>(this TWorkflow workflow, Func<IQueryable<TEntity>, TModel> selector, ModelRegistrationOptions registrationOptions)
            where TEntity : class
            where TWorkflow : EntityWorkflow
        {
            return LoadModel(workflow, selector, null, registrationOptions);
        }

        public static Either<ActionResult, TModel> LoadModel<TWorkflow, TEntity, TModel>(this TWorkflow workflow, Func<IQueryable<TEntity>, TModel> selector, Func<TModel> fallback)
            where TEntity : class
            where TWorkflow : EntityWorkflow
        {
            return LoadModel(workflow, selector, fallback, ModelRegistrationOptions.None);
        }

        public static Either<ActionResult, TModel> LoadModel<TWorkflow, TEntity, TModel>(this TWorkflow workflow, Func<IQueryable<TEntity>, TModel> selector, Action fallbackAction)
            where TEntity : class
            where TWorkflow : EntityWorkflow
        {
            if (fallbackAction != null)
                return LoadModel(workflow, selector, () => { fallbackAction(); return default(TModel); }, ModelRegistrationOptions.None);
            else
                return LoadModel(workflow, selector, null, ModelRegistrationOptions.None);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TWorkflow"></typeparam>
        /// <typeparam name="TEntity"></typeparam>
        /// <typeparam name="TModel"></typeparam>
        /// <param name="workflow"></param>
        /// <param name="selector"></param>
        /// <param name="fallback"></param>
        /// <param name="registrationOptions"></param>
        /// <returns></returns>
        /// <remarks>
        /// Не применяет фильтры, в случае использования этой функции фильтры надо применить вручную.
        /// </remarks>
        public static Either<ActionResult, TModel> LoadModel<TWorkflow, TEntity, TModel>(this TWorkflow workflow, Func<IQueryable<TEntity>, TModel> selector, Func<TModel> fallback, ModelRegistrationOptions registrationOptions)
            where TEntity : class
            where TWorkflow : EntityWorkflow
        {
            if (workflow == null) throw new ArgumentNullException("workflow");
            if (selector == null) throw new ArgumentNullException("selector");

            return Execute(workflow, () =>
            {
                var repository = workflow.GetRepository<TEntity>();
                var model = selector(repository);
                if ((model == null) && (fallback != null))
                    model = fallback();
                RegisterModel(workflow, model, registrationOptions);
                return model;
            });
        }

        /// <summary>
        /// Emmits sort command for the default entity.
        /// </summary>
        /// <typeparam name="TWorkflow">Type of the workflow.</typeparam>
        /// <param name="workflow">Workflow.</param>
        /// <param name="path">Path to the property to sort by.</param>
        /// <returns>
        /// Either workflow if succeed or  <see cref="ActionResult"/> if failed.
        /// </returns>
        public static Either<ActionResult, TWorkflow> SortCommand<TWorkflow>(this TWorkflow workflow, string propertyPath)
            where TWorkflow : EntityWorkflow
        {
            if (workflow == null) throw new ArgumentNullException("workflow");

            return Execute(workflow, () =>
            {
                if (workflow.EntityType == null)
                    throw new InvalidOperationException("Default sort command could not be applied because default entity type not set.");

                var sortExpressionBuilder = workflow.GetEntityService(workflow.EntityType, () => new SortExpressionBuilder(workflow.GetEntityMetadata(workflow.EntityType)));
                sortExpressionBuilder.SortCommand(propertyPath);
            });
        }

        public static Either<ActionResult, IQueryable<TEntity>> SortCommand<TWorkflow, TEntity>(this TWorkflow workflow, IQueryable<TEntity> query, string column)
            where TWorkflow : EntityWorkflow
        {
            if (workflow == null) throw new ArgumentNullException("workflow");

            return Execute(workflow, () =>
            {
                var sortExpressionBuilder = workflow.GetEntityService(typeof(TEntity), () => new SortExpressionBuilder(workflow.GetEntityMetadata(typeof(TEntity))));
                sortExpressionBuilder.SortCommand(column);
                return sortExpressionBuilder.AppendSortExpression(query);
            });
        }

        public static Either<ActionResult, TWorkflow> SetActionResult<TWorkflow>(this TWorkflow workflow, ActionResult actionResult)
            where TWorkflow : EntityWorkflow
        {
            return SetActionResults(workflow, actionResult, actionResult);
        }

        public static Either<ActionResult, TWorkflow> SetActionResults<TWorkflow>(this TWorkflow workflow, ActionResult successResult, ActionResult failureResult)
            where TWorkflow : EntityWorkflow
        {
            if (workflow == null) throw new ArgumentNullException("workflow");

            return Execute(workflow, () =>
            {
                workflow.SuccessResult = successResult;
                workflow.FailureResult = failureResult;
            });
        }

        public static Either<ActionResult, TState> SetControllerState<TWorkflow, TState>(this TWorkflow workflow, string key, TState value, bool promoteToViewData)
            where TWorkflow : EntityWorkflow
        {
            if (workflow == null) throw new ArgumentNullException("workflow");

            return workflow.Execute(() =>
            {
                workflow.ControllerState[key] = value;
                if (promoteToViewData)
                    workflow.ViewData[key] = value;
                return value;
            });
        }

        public static Either<ActionResult, TWorkflow> SetFailureResult<TWorkflow>(this TWorkflow workflow, ActionResult failureResult)
            where TWorkflow : EntityWorkflow
        {
            if (workflow == null) throw new ArgumentNullException("workflow");

            return Execute(workflow, () =>
            {
                workflow.FailureResult = failureResult;
            });
        }

        //TODO: это нахуй!
        public static Either<ActionResult, TModel> SetModel<TWorkflow, TModel>(this TWorkflow workflow, TModel model)
            where TWorkflow : EntityWorkflow
        {
            return SetModel(workflow, model, null);
        }

        //TODO: это нахуй!
        public static Either<ActionResult, TModel> SetModel<TWorkflow, TModel>(this TWorkflow workflow, TModel model, Action<TModel> initModelAction)
            where TWorkflow : EntityWorkflow
        {
            if (workflow == null) throw new ArgumentNullException("workflow");

            return workflow.Execute(() =>
            {
                RegisterModel(workflow, model, ModelRegistrationOptions.Model);
                if (initModelAction != null)
                    initModelAction(model);

                return model;
            });
        }

        public static Either<ActionResult, TModel> SetModel<TWorkflow, TModel>(
            this TWorkflow workflow, 
            TModel model, 
            ModelRegistrationOptions registrationOptions)
            where TWorkflow : EntityWorkflow
        {
            if (workflow == null) throw new ArgumentNullException("workflow");

            return workflow.Execute(() =>
            {
                RegisterModel(workflow, model, registrationOptions);
                return model;
            });
        }

        public static Either<ActionResult, TWorkflow> SetSuccessResult<TWorkflow>(this TWorkflow workflow, ActionResult successResult)
            where TWorkflow : EntityWorkflow
        {
            if (workflow == null) throw new ArgumentNullException("workflow");

            return Execute(workflow, () =>
            {
                workflow.SuccessResult = successResult;
            });
        }

        public static Either<ActionResult, TWorkflow> UpdateEntities<TWorkflow, TEntity>(this TWorkflow workflow, IEnumerable<TEntity> entities)
            where TWorkflow : EntityWorkflow
            where TEntity : class
        {
            if (workflow == null) throw new ArgumentNullException("workflow");

            return Execute(workflow, () =>
            {
                var repository = workflow.GetRepository<TEntity>();
                foreach (var entity in entities)
                    repository.Update(entity);
            });
        }

        //TODO: переименовать в save..
        public static Either<ActionResult, TWorkflow> UpdateEntity<TWorkflow, TEntity>(this TWorkflow workflow, TEntity updatedEntity)
            where TWorkflow : EntityWorkflow
            where TEntity : class
        {
            if (workflow == null) throw new ArgumentNullException("workflow");

            return Execute(workflow, () =>
            {
                var repository = workflow.GetRepository<TEntity>();
                repository.Update(updatedEntity);
            });
        }

        public static Either<ActionResult, TModel> UpdateModel<TWorkflow, TModel>(this TWorkflow workflow, TModel model, params string[] includeProperties)
             where TWorkflow : EntityWorkflow
             where TModel : class
        {
            if (workflow == null) throw new ArgumentNullException("workflow");

            return Execute(workflow, () =>
            {
                if (workflow.Controller.TryUpdateModel(model, includeProperties))
                    return model;
                else
                    return null;
            });
        }

        // ммм.. что-то не очень. Вообще есть желание перекодировать wf, чтобы в монаде было значение, это упростит работу с комбинаторами.
        public static Either<ActionResult, TWorkflow> Where<TWorkflow>(
            this TWorkflow workflow, 
            Func<bool> prediacte,
            Func<Tuple<string, string>> errorProvider)
             where TWorkflow : EntityWorkflow
        {
            return Execute(workflow, () =>
            {
                if (!prediacte())
                {
                    var error = errorProvider();
                    workflow.AddModelError(error.Item1, error.Item2);
                }
            });
        }

        #endregion

        #region Non-monadic

        public static void AddModelError<TWorkflow>(this TWorkflow workflow, string key, string errorMessage)
            where TWorkflow : EntityWorkflow
        {
            if (workflow == null) throw new ArgumentNullException("workflow");

            workflow.ModelState.AddModelError(key, errorMessage);
        }

        public static void AddModelError<TWorkflow>(this TWorkflow workflow, string key, string format, params object[] args)
            where TWorkflow : EntityWorkflow
        {
            AddModelError(workflow, key, string.Format(format, args));
        }
        /*
        public static TService GetEntityService<TWorkflow, TService>(this TWorkflow workflow, Func<TService> serviceProvider)
            where TWorkflow : EntityWorkflow
        {
            if (workflow == null) throw new ArgumentNullException("workflow");

            return workflow.GetEntityService<TService>(workflow.EntityType, serviceProvider);
        }
        */
        #endregion

        private static void RegisterModel<TWorkflow, TModel>(this TWorkflow workflow, TModel model, ModelRegistrationOptions registrationOptions)
            where TWorkflow : EntityWorkflow
        {
            if (workflow == null) throw new ArgumentNullException("workflow");

            if ((registrationOptions.Target & ModelRegistrationTarget.Model) == ModelRegistrationTarget.Model)
                workflow.Model = model;

            if ((registrationOptions.Target & ModelRegistrationTarget.ViewData) == ModelRegistrationTarget.ViewData)
                workflow.ViewData.SetInstance<TModel>(model, registrationOptions.Name, registrationOptions.Type);
        }
    }
}